Skip to content

Conversation

@lumoswiz
Copy link
Collaborator

@lumoswiz lumoswiz commented Nov 3, 2025

Summary

Add ERC6909Facet and LibERC6909 implementations adhering to ERC-6909.

A minimal styling favouring panic via arithmetic checks in lieu of custom errors was used here. Please see additional notes below for alternative styles.

Testing to be done over the next few days.

Changes Made

Added:

  • ERC6909Facet.sol
  • LibERC6909.sol

Checklist

Before submitting this PR, please ensure:

  • Code follows the Solidity feature ban - No inheritance, constructors, modifiers, public/private variables, external library functions, using for directives, or selfdestruct

  • Code follows Design Principles - Readable, uses diamond storage, favors composition over inheritance

  • Code matches the codebase style - Consistent formatting, documentation, and patterns (e.g. ERC20Facet.sol)

  • Code is formatted with forge fmt

  • Tests are included - All new functionality has comprehensive tests

  • All tests pass - Run forge test and ensure everything works

  • Documentation updated - If applicable, update relevant documentation

Make sure to follow the CONTRIBUTING.md guidelines.

Additional Notes

There are multiple ERC-6909 implementations available, with slightly different styles. See links below:

I have opted for the minimal Uniswap V4/Solmate implementation, however, I am happy to adapt to whatever styling you prefer. Please let me know.

@github-actions
Copy link

github-actions bot commented Nov 3, 2025

Coverage Report

Coverage

Metric Coverage Details
Lines 82% 1754/2139 lines
Functions 85% 333/391 functions
Branches 69% 252/367 branches

Last updated: Tue, 25 Nov 2025 21:03:22 GMT for commit 82481d0

@github-actions
Copy link

github-actions bot commented Nov 3, 2025

Gas Report

Comparing gas usage between main and feat/erc6909

Summary

  • Optimized: 47 functions (🟢 -5,189,522 gas)
  • Increased: 42 functions (🔴 +1,355 gas)
  • Net Change: 🟢 -5,188,167 gas

Details

Contract Function Before After Change
Test test_DiamondCut_initializeCall() N/A N/A 🟢 -518,933
Test test_DiamondCut_replaceAction() N/A N/A 🟢 -518,915
Test test_DiamondCut_initalizeCallWithWrongCalldataReturningErrorMessage() N/A N/A 🟢 -518,863
Test test_diamondCut_InitializeCallWithWrongCalldataReturningErrorMessage() N/A N/A 🟢 -518,862
Test test_DiamondCut_initializeCallWithWrongCalldata() N/A N/A 🟢 -518,862
Test test_diamondCut_InitializeCallWithWrongCalldata() N/A N/A 🟢 -518,861
Test test_replaceFunctions_ReplaceFunctionThatDoesNotExists() N/A N/A 🟢 -518,837
Test test_replaceFunctions() N/A N/A 🟢 -518,837
Test test_diamondCut_MultipleActions() N/A N/A 🟢 -518,836
Test test_DiamondCut_multipleActions() N/A N/A 🟢 -518,721
Test test_TransferFrom_UnlimitedAllowance() N/A N/A 🔴 +104
Test testFuzz_SequentialTransfers() N/A N/A 🔴 +85
Test test_RevertWhen_TransferOverflowsRecipient() N/A N/A 🟢 -84
Test test_TransferOwnership_EmitsCorrectPreviousOwner() N/A N/A 🔴 +78
Test test_RevertWhen_TransferOwnership_CalledByNonOwner() N/A N/A 🔴 +73
Test test_Name() N/A N/A 🟢 -73
Test test_TransferOwnership_ToSelf() N/A N/A 🔴 +72
Test test_RenounceOwnership_SetsOwnerToZero() N/A N/A 🔴 +71
Test test_TransferFrom_PartialAllowance() N/A N/A 🔴 +69
Test test_TotalSupply() N/A N/A 🟢 -67

Showing top 20 changes out of 89 total.

View all 89 changes
Contract Function Before After Change
Test test_DiamondCut_initializeCall() N/A N/A 🟢 -518,933
Test test_DiamondCut_replaceAction() N/A N/A 🟢 -518,915
Test test_DiamondCut_initalizeCallWithWrongCalldataReturningErrorMessage() N/A N/A 🟢 -518,863
Test test_diamondCut_InitializeCallWithWrongCalldataReturningErrorMessage() N/A N/A 🟢 -518,862
Test test_DiamondCut_initializeCallWithWrongCalldata() N/A N/A 🟢 -518,862
Test test_diamondCut_InitializeCallWithWrongCalldata() N/A N/A 🟢 -518,861
Test test_replaceFunctions_ReplaceFunctionThatDoesNotExists() N/A N/A 🟢 -518,837
Test test_replaceFunctions() N/A N/A 🟢 -518,837
Test test_diamondCut_MultipleActions() N/A N/A 🟢 -518,836
Test test_DiamondCut_multipleActions() N/A N/A 🟢 -518,721
Test test_TransferFrom_UnlimitedAllowance() N/A N/A 🔴 +104
Test testFuzz_SequentialTransfers() N/A N/A 🔴 +85
Test test_RevertWhen_TransferOverflowsRecipient() N/A N/A 🟢 -84
Test test_TransferOwnership_EmitsCorrectPreviousOwner() N/A N/A 🔴 +78
Test test_RevertWhen_TransferOwnership_CalledByNonOwner() N/A N/A 🔴 +73
Test test_Name() N/A N/A 🟢 -73
Test test_TransferOwnership_ToSelf() N/A N/A 🔴 +72
Test test_RenounceOwnership_SetsOwnerToZero() N/A N/A 🔴 +71
Test test_TransferFrom_PartialAllowance() N/A N/A 🔴 +69
Test test_TotalSupply() N/A N/A 🟢 -67
Test testFuzz_RenouncePreventsAllTransfers() N/A N/A 🔴 +57
Test test_Approve_ZeroAmount() N/A N/A 🟢 -53
Test test_TransferOwnership_FromPendingOwner_Allowed() N/A N/A 🟢 -44
Test testFuzz_TransferOwnership() N/A N/A 🟢 -44
Test test_TransferOwnership_AllowsTransferToZeroAddress() N/A N/A 🟢 -44
Test test_RenounceOwnership_PreventsNewTransfersAfterForceRenounce() N/A N/A 🟢 -44
Test test_GetStorage_ReturnsCorrectOwner() N/A N/A 🟢 -44
Test test_StorageCollision_DocumentedBug() N/A N/A 🔴 +44
Test test_PendingOwner_ReturnsCurrentPendingOwner() N/A N/A 🔴 +42
Test test_RevertWhen_TransferFromZeroAddressReceiver() N/A N/A 🔴 +41
Test test_RevertWhen_TransferFromInsufficientBalance() N/A N/A 🔴 +37
Test test_Symbol() N/A N/A 🟢 -37
Test test_TransferFrom_UnlimitedAllowance_MultipleTransfers() N/A N/A 🔴 +36
Test test_CancelPendingTransfer() N/A N/A 🔴 +36
Test test_AcceptOwnership_CurrentOwnerCanCall() N/A N/A 🟢 -35
Test test_Decimals() N/A N/A 🟢 -35
Test test_BalanceOf() N/A N/A 🔴 +32
Test test_RevertWhen_TransferFromZeroBalance() N/A N/A 🟢 -28
Test test_Transfer() N/A N/A 🔴 +26
Test testFuzz_TransferFrom() N/A N/A 🔴 +25
Test test_Approve_UpdateExisting() N/A N/A 🟢 -23
Test test_Approve() N/A N/A 🟢 -23
Test test_TransferOwnership_EmitsOwnershipTransferStartedEvent() N/A N/A 🔴 +23
Test testFuzz_Approve() N/A N/A 🟢 -23
Test test_RenounceOwnership_CannotBeCompleted() N/A N/A 🔴 +23
Test test_StorageSlot_CurrentlyCollides() N/A N/A 🔴 +23
Test test_TransferOwnership_CanUpdatePendingOwner() N/A N/A 🟢 -22
Test test_GetStorage_ReturnsCorrectPendingOwner() N/A N/A 🟢 -22
Test test_TransferOwnership_LibraryDoesNotCheckCaller() N/A N/A 🔴 +22
Test test_StorageSlot_UsesCorrectPosition() N/A N/A 🟢 -22
Test testFuzz_TransferOwnership_AnyCaller() N/A N/A 🔴 +22
Test test_AcceptOwnership_NoPendingOwner_SetsOwnerToZero() N/A N/A 🟢 -22
Test test_RevertWhen_TransferOwnership_FromRenouncedOwner() N/A N/A 🔴 +22
Test test_Owner_ReturnsCurrentOwner() N/A N/A 🔴 +22
Test test_Owner_ReturnsCorrectInitialOwner() N/A N/A 🟢 -22
Test test_Approve_ReturnsTrue() N/A N/A 🔴 +21
Test test_Transfer_ZeroAmount() N/A N/A 🟢 -20
Test testFuzz_SequentialTransfers() N/A N/A 🔴 +19
Test test_RevertWhen_TransferFromNoAllowance() N/A N/A 🔴 +19
Test test_RevertWhen_TransferToZeroAddress() N/A N/A 🔴 +19
Test test_SequentialTransfers() N/A N/A 🔴 +18
Test test_AcceptOwnership_LibraryAllowsAnyCaller() N/A N/A 🟢 -18
Test testFuzz_AcceptOwnership_AnyCaller() N/A N/A 🔴 +18
Test test_AcceptOwnership_ClearsPendingOwner() N/A N/A 🔴 +18
Test testFuzz_AcceptOwnership() N/A N/A 🔴 +18
Test test_AcceptOwnership_EmitsOwnershipTransferredEvent() N/A N/A 🔴 +18
Test test_TransferOwnership_MultipleTransfers() N/A N/A 🔴 +18
Test test_RenounceOwnership_PreventsAllFurtherTransfers() N/A N/A 🔴 +18
Test test_MultiplePendingChanges_OnlyLastOneMatters() N/A N/A 🟢 -17
Test test_AcceptOwnership_CompletesTransfer() N/A N/A 🔴 +17
Test test_TransferToSelf() N/A N/A 🟢 -17
Test test_TransferOwnership_EmitsOwnershipTransferredEvent() N/A N/A 🟢 -16
Test testFuzz_TransferOwnership() N/A N/A 🟢 -16
Test test_RenounceOwnership_EmitsEvent() N/A N/A 🟢 -16
Test testFuzz_RevertWhen_UnauthorizedCaller() N/A N/A 🟢 -16
Test testFuzz_Transfer() N/A N/A 🔴 +14
Test test_TransferFrom_ReturnsTrue() N/A N/A 🔴 +12
Test test_Transfer_ToSelf() N/A N/A 🟢 -12
Test test_RevertWhen_TransferOwnership_CalledByPreviousOwner() N/A N/A 🟢 -10
Test test_Transfer_ReturnsTrue() N/A N/A 🟢 -7
Test test_Transfer_EntireBalance() N/A N/A 🟢 -7
Test test_StorageSlot_Consistency() N/A N/A 🔴 +7
Test test_TransferOwnership_ImmediateTransfer() N/A N/A 🔴 +6
Test test_Owner_ReturnsZeroWhenRenounced() N/A N/A 🔴 +6
Test test_RevertWhen_TransferInsufficientBalance() N/A N/A 🟢 -5
Test test_TransferFrom() N/A N/A 🔴 +4
Test test_RevertWhen_TransferFromZeroAddressSender() N/A N/A 🟢 -3
Test test_RevertWhen_TransferFromInsufficientAllowance() N/A N/A 🟢 -2
Test test_RevertWhen_ApproveZeroAddressSpender() N/A N/A 🟢 -2
ℹ️ About this report

This report compares gas usage between the base branch and this PR using forge snapshot.

  • 🟢 indicates a gas improvement (reduction)
  • 🔴 indicates a gas regression (increase)
  • Functions not shown have unchanged gas costs

To run this locally:

# Generate snapshot for current branch
forge snapshot

# Compare with another branch
git checkout main
forge snapshot --diff .gas-snapshot

Last updated: Tue, 25 Nov 2025 21:03:58 GMT for commit 82481d0

@mudgen
Copy link
Contributor

mudgen commented Nov 3, 2025

Great! Will look at this. I am glad to have an ERC-6909 implementation.

@lumoswiz
Copy link
Collaborator Author

lumoswiz commented Nov 5, 2025

Forgot to add, after I have finalised the styling and testing of the base ERC-6909, I'll add the extensions (token supply, metadata and content URI).

@mudgen
Copy link
Contributor

mudgen commented Nov 8, 2025

@lumoswiz Just want you to know, I am happy this pull request was submitted, because I want Compose to provide this ERC functionality. I and/or another reviewer will get around to this pull request.

Copy link
Contributor

@mudgen mudgen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking great!


if (_by != address(0) && !s.isOperator[_from][_by]) {
uint256 allowed = s.allowance[_from][_by][_id];
if (allowed != type(uint256).max) s.allowance[_from][_by][_id] = allowed - _amount;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use braces here, for example:

if (allowed != type(uint256).max)) {
    s.allowance[_from][_by][_id] = allowed - _amount;
}

ERC6909Storage storage s = getStorage();
if (msg.sender != _sender && !s.isOperator[_sender][msg.sender]) {
uint256 allowed = s.allowance[_sender][msg.sender][_id];
if (allowed != type(uint256).max) s.allowance[_sender][msg.sender][_id] = allowed - _amount;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use braces here please.

@mudgen mudgen marked this pull request as ready for review November 16, 2025 20:55
Copilot AI review requested due to automatic review settings November 16, 2025 20:55
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds an ERC-6909 multi-token standard implementation using the diamond storage pattern. The implementation follows a minimal style that favors arithmetic panics over custom errors, adapted from the Uniswap V4/Solmate implementation.

Key changes:

  • Added LibERC6909.sol providing internal functions (mint, burn, transfer, approve, setOperator) for ERC-6909 token operations
  • Added ERC6909Facet.sol exposing the public ERC-6909 interface (balanceOf, allowance, isOperator, transfer, transferFrom, approve, setOperator)

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
src/token/ERC6909/LibERC6909.sol Library containing ERC-6909 internal functions and diamond storage layout for multi-token operations
src/token/ERC6909/ERC6909Facet.sol Facet implementing the public ERC-6909 interface using diamond storage pattern

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


if (_by != address(0) && !s.isOperator[_from][_by]) {
uint256 allowed = s.allowance[_from][_by][_id];
if (allowed != type(uint256).max) s.allowance[_from][_by][_id] = allowed - _amount;
Copy link

Copilot AI Nov 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one-line if statement violates the project's style guide (STYLE.md section 5), which requires all if statements to use brackets and a newline. The statement should be reformatted as:\nsolidity\nif (allowed != type(uint256).max) {\n s.allowance[_from][_by][_id] = allowed - _amount;\n}\n

Suggested change
if (allowed != type(uint256).max) s.allowance[_from][_by][_id] = allowed - _amount;
if (allowed != type(uint256).max) {
s.allowance[_from][_by][_id] = allowed - _amount;
}

Copilot uses AI. Check for mistakes.
ERC6909Storage storage s = getStorage();
if (msg.sender != _sender && !s.isOperator[_sender][msg.sender]) {
uint256 allowed = s.allowance[_sender][msg.sender][_id];
if (allowed != type(uint256).max) s.allowance[_sender][msg.sender][_id] = allowed - _amount;
Copy link

Copilot AI Nov 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one-line if statement violates the project's style guide (STYLE.md section 5), which requires all if statements to use brackets and a newline. The statement should be reformatted as:\nsolidity\nif (allowed != type(uint256).max) {\n s.allowance[_sender][msg.sender][_id] = allowed - _amount;\n}\n

Suggested change
if (allowed != type(uint256).max) s.allowance[_sender][msg.sender][_id] = allowed - _amount;
if (allowed != type(uint256).max) {
s.allowance[_sender][msg.sender][_id] = allowed - _amount;
}

Copilot uses AI. Check for mistakes.

/// @title LibERC6909 — ERC-6909 Library
/// @notice Provides internal functions and storage layout for ERC-6909 minimal multi-token logic.
/// @dev Uses ERC-8042 for storage location standardization and ERC-6093 for error conventions.
Copy link

Copilot AI Nov 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment claims to use 'ERC-6093 for error conventions', but the implementation does not define or use any custom errors (it relies on arithmetic panics instead). Either remove the ERC-6093 reference from the documentation or add appropriate custom errors as done in other token implementations (see LibERC20.sol for reference).

Suggested change
/// @dev Uses ERC-8042 for storage location standardization and ERC-6093 for error conventions.
/// @dev Uses ERC-8042 for storage location standardization.

Copilot uses AI. Check for mistakes.
@lumoswiz
Copy link
Collaborator Author

Thanks for the feedback, I'm on it.

@mudgen do you have a preference for how to add the ERC-6909 extensions? Should I add them in this PR, or in separate PRs?

I'll make the changes as above, and then add tests this week.

@mudgen
Copy link
Contributor

mudgen commented Nov 17, 2025

@lumoswiz Please submit each extension as a separate pull request. That makes it easier for them to review.

I look forward to the extensions. This is great.

By the way, we released new documentation here:
https://compose.diamonds/docs/design/
https://compose.diamonds/docs/contribution/how-to-contribute

I am interested if feedback on this new documentation, if you have any feedback to give about it.

@netlify
Copy link

netlify bot commented Nov 19, 2025

Deploy Preview for compose-diamonds ready!

Name Link
🔨 Latest commit 50e8e21
🔍 Latest deploy log https://app.netlify.com/projects/compose-diamonds/deploys/6927086850d2620008a3ed3f
😎 Deploy Preview https://deploy-preview-167--compose-diamonds.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@lumoswiz lumoswiz marked this pull request as draft November 19, 2025 10:31
@Perfect-Abstractions Perfect-Abstractions deleted a comment from Copilot AI Nov 22, 2025
@mudgen
Copy link
Contributor

mudgen commented Nov 22, 2025

Wow, panic via arithmetic checks, instead of custom errors is so clean. Maybe we should do that for all kinds of token transfers ERC1155 and ERC20.

@maxnorm
Copy link
Collaborator

maxnorm commented Nov 22, 2025

Wow, panic via arithmetic checks, instead of custom errors is so clean. Maybe we should do that for all kinds of token transfers ERC1155 and ERC20.

Hi, it's really clever but does that break compatibility where project expect a different custom error for InsufficientBalance & InsufficientAllowance? It remove the clarity on the error cause.

just wondering

@mudgen
Copy link
Contributor

mudgen commented Nov 22, 2025

@maxnorm Yes it could. And I think a Panic(0x11) could be more difficult to debug in some cases.

Copy link
Contributor

@mudgen mudgen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lumoswiz I reviewed the code and I like it. Good job.

While I like the cleanness of using the builtin Solidity arithmetic panic, I think we should use the following errors that OZ uses for this implementation:

error ERC6909InsufficientBalance(address _sender, uint256 _balance, uint256 _needed, uint256 _id);
error ERC6909InsufficientAllowance(address _spender, uint256 _allowance, uint256 _needed, uint256 _id);
error ERC6909InvalidApprover(address _approver);
error ERC6909InvalidReceiver(address _receiver);
error ERC6909InvalidSender(address _sender);
error ERC6909InvalidSpender(address _spender);

Can you change the code to use these?

@maxnorm
Copy link
Collaborator

maxnorm commented Nov 22, 2025

Can you also add the interface IERC6909 inside the src/interfaces folder? We don't inherit from them but our user will need them to interact with the facet easily

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/interfaces/IERC6909.sol

@lumoswiz
Copy link
Collaborator Author

I can add the interfaces and errors.

Would you like this implementation to then essentially mirror OZ? This current implementation doesn't guard against the zero address (e.g. in LibERC6909.burn the from addr can be address(0). In OZ ERC6909, there is an explicit guard for this, see here.

Without these checks, some of the errors mentioned above don't make sense to include.

@mudgen @maxnorm Let me know what you think, I am happy to refactor this to more closely mirror the OZ impl.

@mudgen
Copy link
Contributor

mudgen commented Nov 24, 2025

@lumoswiz
Let's guard against using the zero address and use all the errors.

But the code should be written differently than OpenZepplen. Please review the documentation for how the code should be written. Specifically:

@lumoswiz
Copy link
Collaborator Author

@mudgen Added the errors and interface. The ERC6909InvalidApprover error wasn't added to ERC6909Facet since msg.sender != address(0). It was used in LibERC6909.

Copy link
Contributor

@mudgen mudgen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lumoswiz
Looks good! Good job. Please make the requested change.

Comment on lines 127 to 133
if (_sender == address(0)) {
revert ERC6909InvalidSender(address(0));
}

if (_receiver == address(0)) {
revert ERC6909InvalidReceiver(address(0));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is different than the OZ implementation, but please put these sender and receiver checks at the beginning of this function. Now our implementation is better than OZ.

Comment on lines 121 to 127
if (_from == address(0)) {
revert ERC6909InvalidSender(address(0));
}

if (_to == address(0)) {
revert ERC6909InvalidReceiver(address(0));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please put these functions at the beginning of the function.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

@mudgen mudgen merged commit e497d22 into Perfect-Abstractions:main Nov 26, 2025
4 of 8 checks passed
@mudgen
Copy link
Contributor

mudgen commented Nov 26, 2025

Good job!

@mudgen
Copy link
Contributor

mudgen commented Nov 26, 2025

@lumoswiz We have an ERC6909 test failing. Can you check it out?
Screenshot from 2025-11-26 09-33-06

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants